/*
 * The MIT License (MIT)
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package multiswarm;

import java.util.Random;

import box2dLight.PointLight;
import box2dLight.RayHandler;
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputMultiplexer;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture.TextureFilter;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.collision.Ray;
import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.utils.Array;
import com.multiswarmgdx.MultiAgentMenuScreen;

/**
 * This is the Arena class. It manages almost anything.
 * TODO: better documentation here.
 * 
 * @author ralfm
 */
public abstract class Arena implements Screen, InputProcessor {
	protected Random rand = new Random();
	
	/**
	 * The width and height of the arena.
	 */
	protected float w;
	protected float h;
	
	/**
	 * Game variable that can be used to switch arenas at runtime.
	 * To use it replace your main ApplicationListener with a Game object and pass
	 * it in the constructor.
	 */
	protected Game game;
	
	/**
	 * Our box2d physics world. This gets initialized in @see {@link #show()} and uses an instance of
	 * AgentContactListener to listen for collisions update the sensors.
	 */
	public World world;
	
	/**
	 * Fixed time Step variables.
	 * @see #render(float)
	 */
    /*final static*/ float FIXED_TIMESTEP = 1.0f/120.0f;
	float fixedTimestepAccumulator_;
    float fixedTimestepAccumulatorRatio_;
    
    /**
     * This is our @see {@link Stage}, for handling touches, rendering, etc.
     */
	public Stage stage;

	/**
	 * SpriteBatch used for rendering the agent sprites in @see {@link #render(float)}.
	 */
	protected SpriteBatch batch;

	/**
	 * Texture atlas and shared agentTexture. 
	 * TODO: enable multiple agent textures for one arena.
	 */
	protected TextureAtlas atlas;
	public TextureRegion agentTexture;
	
	/**
	 * The main camera for this arena, and a field to following a paticular agent.
	 * Just set agentToFollow to an agent and the camera is locked onto the agent in @see {@link #render(float)}.
	 */
	public OrthographicCamera camera;
	public Agent agentToFollow = null;
	
	/**
	 * Our custom shader.
	 */
	public ShaderProgram agentShader = null;

	/**
	 * The input Multiplexer for handling touches.
	 */
	protected InputMultiplexer inputMultiplexer;
	
	/**
	 * Box2dLight RayHandler
	 * {@link https://code.google.com/p/box2dlights/}
	 */
    public RayHandler rayHandler;
	public boolean renderLight = false;
	
	/**
	 * Our debug renderer, which renders all physics bodies, including sensors.
	 */
	protected Box2DDebugRenderer debugRenderer;
	public boolean renderDebug = false;
	
	/**
	 * Label stage used for rendering the FPS label on the bottom of the screen.
	 * This is switched off when Debug render is disabled.
	 */
	protected Stage fpsLabelStage;
	Label fpsLabel;
	float fpsLabelUpdateTimer=0;

	/**
	 * Arrays and variables to keep track of all active Touches and Lights.
	 */
	protected Array<AgentTouch> activeTouches = new Array<AgentTouch>();
	//protected Array<PointLight> activeLights = new Array<PointLight>();
	protected AgentTouch lastDragTouch;
	
	/**
	 * Constructor to keep a reference of the parent game. This is used if you spawn the
	 * arena from a Game object to switch them during runtime.
	 * @param parentGame
	 */
	public Arena(Game parentGame) {
		game = parentGame;
	}
	
	/**
	 * Default constructor.
	 */
	public Arena() {
		game = null;
	}

	/**
	 * Show method is called when the arena is initalized.
	 * What is initalized: 
	 *   - Camera with width and height from Gdx.graphics.
	 *   - Box2D World.
	 *   - RayHandler to handle light rendering.
	 *   - DebugRenderer to handle Box2D debug rendering.
	 *   - FPS Label
	 *   - Custom Blur Shader.
	 *   - Sprite Batch for Sprite Rendering.
	 *   - Agent Texture from atlas for efficient drawing.
	 *   - Scene2D Stage for managing agents efficiently.
	 *   - Input Multiplexer
	 */
	@Override
	public void show() {
		//Initialize camera with the width and height from our Gdx Window
		//TODO: It should be determined if it's more efficient to divide these numbers by a constant to keep 
		//      the Box2D body sizes down.
		w = Gdx.graphics.getWidth();
		h = Gdx.graphics.getHeight();
		
		camera = new OrthographicCamera(w, h);
		camera.position.set(w * 0.5f, h * 0.5f, 0);
		camera.update();
		
		//Initialize our world with zero gravity and set our Contact Listener.
		world = new World(new Vector2(0.0f, 0.0f), true);
		world.setContactListener(new AgentContactListener());
		
		//Initialize RayHandler with black ambient light. This let's us see our agents even when no
		//lights are present.
		rayHandler = new RayHandler(world);
		rayHandler.setAmbientLight(new Color(0f,0f,0f,1.0f));
		rayHandler.setCombinedMatrix(camera.combined);
		RayHandler.setGammaCorrection(true);
		
		
		//Initialize Debug Renderer.
		debugRenderer = new Box2DDebugRenderer();
		//FPS Label Init
		Table table = new Table();
		Skin skin = new Skin(Gdx.files.internal("graphics/uiskin.json"));
		skin.getAtlas().getTextures().iterator().next().setFilter(TextureFilter.Nearest, TextureFilter.Nearest);
		fpsLabel = new Label("", skin);
		table.setPosition(65, 10);
		table.add(fpsLabel);
		
		fpsLabelStage = new Stage();
		fpsLabelStage.addActor(table);

		//Inizialize Sprite Shader
		//This is a very basic blur shader.
		String vertexShader = "attribute vec4 " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" //
				+ "attribute vec4 " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" //
				+ "attribute vec2 " + ShaderProgram.TEXCOORD_ATTRIBUTE + "0;\n" //
				+ "uniform mat4 u_projTrans;\n" //
				+ "varying vec4 v_color;\n" //
				+ "varying vec2 v_texCoords;\n" //
				+ "\n" //
				+ "void main()\n" //
				+ "{\n" //
				+ "   v_color = " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" //
				+ "   v_texCoords = " + ShaderProgram.TEXCOORD_ATTRIBUTE + "0;\n" //
				+ "   gl_Position =  u_projTrans * " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" //
				+ "}\n";
			String fragmentShader = "#ifdef GL_ES\n" //
				+ "#define LOWP lowp\n" //
				+ "precision mediump float;\n" //
				+ "#else\n" //
				+ "#define LOWP \n" //
				+ "#endif\n" //
				+ "varying LOWP vec4 v_color;\n" //
				+ "varying vec2 v_texCoords;\n" //
				+ "uniform sampler2D u_texture;\n" //
				+ "uniform vec2 blurSize;\n" //
				//+ "uniform vec4 substract;\n" //
				+ "void main()\n"//
				+ "{\n" //
				//+ "vec2 blurSize = vec2(0.06,0.06);\n" //
				+ "vec4 substract = vec4(0.0);\n" //
				+ "vec4 sum = vec4(0.0);\n" //
				+ "sum += texture2D(u_texture, v_texCoords - 4.0 * blurSize) * 0.05;\n" //
				+ "sum += texture2D(u_texture, v_texCoords - 3.0 * blurSize) * 0.09;\n" //
				+ "sum += texture2D(u_texture, v_texCoords - 2.0 * blurSize) * 0.12;\n" //
				+ "sum += texture2D(u_texture, v_texCoords - 1.0 * blurSize) * 0.15;\n" //
				+ "sum += texture2D(u_texture, v_texCoords                 ) * 0.16;\n" //
				+ "sum += texture2D(u_texture, v_texCoords + 1.0 * blurSize) * 0.15;\n" //
				+ "sum += texture2D(u_texture, v_texCoords + 2.0 * blurSize) * 0.12;\n" //
				+ "sum += texture2D(u_texture, v_texCoords + 3.0 * blurSize) * 0.09;\n" //
				+ "sum += texture2D(u_texture, v_texCoords + 4.0 * blurSize) * 0.05;\n" //
				+ "gl_FragColor = (sum - substract) * v_color * texture2D(u_texture, v_texCoords);\n" //
				//+ "  gl_FragColor = v_color * texture2D(u_texture, v_texCoords);\n" //
				+ "}";

		if (Gdx.graphics.isGL20Available()) 
			agentShader = new ShaderProgram(vertexShader, fragmentShader);						

		if (agentShader != null) {
		  Gdx.app.log("Shader Compiled: ", ""+agentShader.isCompiled() + "  " + agentShader.getLog());
		  //Initialize our SpriteBatch with the custom shader.
		  batch = new SpriteBatch(100, agentShader);
		} else {
		  batch = new SpriteBatch();
		}
		
		//Agent Texture initalization
		//TODO: Implement some kind of efficient Mip Mapping
		atlas = new TextureAtlas(Gdx.files.internal("graphics/agent_atlas.atlas"), Gdx.files.internal("graphics/"));
		agentTexture = atlas.findRegion("agent_default");
		
		
		//Scene2D Initalization
		stage = new Stage(camera.viewportWidth, camera.viewportHeight, true, batch);
		
		//Input Multiplexer Initialization.
		inputMultiplexer = new InputMultiplexer(stage,this);
		Gdx.input.setInputProcessor(inputMultiplexer);
		

	}
	
	/**
	 * Dispose of everything we've allocated.
	 */
	@Override
	public void dispose() {
		stage.dispose();
		batch.dispose();
		world.dispose();
		rayHandler.dispose();
		fpsLabelStage.dispose();
		atlas.dispose();
	}
	
	@Override
	public void hide() {
		dispose();
	}

	/**
	 * Our main render loop. This updates ...
	 */
	@Override
	public void render(float delta) {		
		Gdx.gl.glClearColor(0, 0, 0, 1);
		Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);

		if (agentToFollow != null) {
			camera.position.set(agentToFollow.getX()+(agentToFollow.getWidth()/2), agentToFollow.getY()+(agentToFollow.getHeight()/2), 0);
			camera.update();
			stage.setCamera(camera);
			rayHandler.setCombinedMatrix(camera.combined);
		}
		
		rayHandler.update();
		if (renderLight) rayHandler.render();
		
		stage.act(Gdx.graphics.getDeltaTime());
		stage.draw();
		

		
		//if (renderLight) rayHandler.updateAndRender();
		
		
		fpsLabelUpdateTimer += Gdx.graphics.getDeltaTime();
		if (fpsLabelUpdateTimer > 1.5f) {
 		   fpsLabel.setText("FPS: " + Gdx.graphics.getFramesPerSecond() + " Agents: " + stage.getActors().size);
		   fpsLabelUpdateTimer = 0;
		}

		fpsLabelStage.draw();
		
		if (renderDebug) {
			debugRenderer.render(world, camera.combined);
	

		}

		//Fixed Timestep implementation:
		final int MAX_STEPS = 5;
		  
		fixedTimestepAccumulator_ += Gdx.graphics.getDeltaTime();
		final int nSteps = (int)Math.floor(fixedTimestepAccumulator_ / FIXED_TIMESTEP);
	                                       
		if (nSteps > 0)
		{
			fixedTimestepAccumulator_ -= nSteps * FIXED_TIMESTEP;
		}
		
		fixedTimestepAccumulatorRatio_ = fixedTimestepAccumulator_ / FIXED_TIMESTEP;
		
		// This is similar to clamp "dt":
		// dt = std::min (dt, MAX_STEPS * FIXED_TIMESTEP)
		// but it allows above calculations of fixedTimestepAccumulator_ and
		// fixedTimestepAccumulatorRatio_ to remain unchanged.
		final int nStepsClamped = Math.min(nSteps, MAX_STEPS);
		for (int i = 0; i < nStepsClamped; ++ i)
		{
			// In singleStep_() the CollisionManager could fire custom
			// callbacks that uses the smoothed states. So we must be sure
			// to reset them correctly before firing the callbacks.
			//---------------------------------------------------------------------resetSmoothStates_ ();
			singleStepWorld(FIXED_TIMESTEP);
		}
	  
		world.clearForces();
	}
	
	public void singleStepWorld(float dt) {
		world.step(dt, 6, 2);
	}

	@Override
	public void resize(int width, int height) {
		w = width;
		h = height; //WAS /5
		fpsLabelStage.setViewport(width, height, true);
		stage.setViewport(width, height, true);
		camera.position.set(w * 0.5f, h * 0.5f, 0);

		camera.update();
		stage.setCamera(camera);
		rayHandler.setCombinedMatrix(camera.combined);
		
	}

	@Override
	public void pause() {
		Gdx.app.exit(); //exit on pause for debug.
	}
	
	@Override
	public boolean touchDown(int screenX, int screenY, int pointer, int button) {
		AgentTouch touch = new AgentTouch(pointer);
		Ray ray = camera.getPickRay(screenX, screenY);
		touch.x = ray.origin.x;
		touch.y = ray.origin.y;

		activeTouches.add(touch);

		//Return false so that the touch is handed down to the agents.
		return false;
	}

	@Override
	public boolean touchUp(int screenX, int screenY, int pointer, int button) {		
		AgentTouch touch = new AgentTouch(pointer);
		Ray ray = camera.getPickRay(screenX, screenY);
		touch.x = ray.origin.x;
		touch.y = ray.origin.y;
		
		int touchIndex = activeTouches.indexOf(touch, false);		
		activeTouches.removeIndex(touchIndex);
		
		//Return false so that the touch is handed down to the agents.
		return false;
	}

	
	
	@Override
	public boolean touchDragged(int screenX, int screenY, int pointer) {
		AgentTouch touch = new AgentTouch(pointer);
		touch.id = pointer;
		Ray ray = camera.getPickRay(screenX, screenY);
		touch.x = ray.origin.x;
		touch.y = ray.origin.y;
		
		int touchIndex = activeTouches.indexOf(touch, false);

		activeTouches.set(touchIndex, touch);
		
		//Return false so that the touch is handed down to the agents.
		return false;
	}

	@Override
	public void resume() {
	}
	
	@Override
	public boolean keyDown(int keycode) {
		return false;
	}

	@Override
	public boolean keyUp(int keycode) {
		return false;
	}

	/**
	 * Key handling
	 *  F: Follow random agent
	 *  G: Reset following of agent
	 *  L: Switch rendering of Light
	 *  D: Switch debug rendering.
	 * @param character
	 */
	@Override
	public boolean keyTyped(char character) {
		// Debug Key presses
		if(Gdx.input.isKeyPressed(Input.Keys.F)) {
			if (agentToFollow != null) agentToFollow.isFollowed = false;
			agentToFollow = (Agent) stage.getActors().random();
			agentToFollow.isFollowed = true;
		}
		
		if(Gdx.input.isKeyPressed(Input.Keys.G)) {
			agentToFollow = null;
		}
		
		if(Gdx.input.isKeyPressed(Input.Keys.L)) {
		 	renderLight = !renderLight;
		}
		
		if(Gdx.input.isKeyPressed(Input.Keys.D)) {
			renderDebug = !renderDebug;
		}
		
		if(Gdx.input.isKeyPressed(Input.Keys.Q)) {
			FIXED_TIMESTEP += 0.001f;
			if (FIXED_TIMESTEP>0.5f) FIXED_TIMESTEP = 0.5f;

		}
		
		if(Gdx.input.isKeyPressed(Input.Keys.W)) {
			FIXED_TIMESTEP -= 0.001f;
			if (FIXED_TIMESTEP<1f/285f) FIXED_TIMESTEP = 1f/285f;
			
		}
		
		Gdx.app.log("TS:", ""+FIXED_TIMESTEP);
		 
		 camera.update();
		 stage.setCamera(camera);
		 rayHandler.setCombinedMatrix(camera.combined);
		 
		 return true;
	}


}
